/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkFile, AstTreeUtils, ts } from "arkanalyzer/lib";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { Defects, IssueReport } from "../../model/Defects";
import { MatcherCallback, MatcherTypes, MethodMatcher, FileMatcher } from "../../matcher/Matchers";
import { Rule } from "../../model/Rule";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { RuleFix } from '../../model/Fix';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'TypeAnnotationSpacingCheck');

interface WhitespaceRule {
    before?: boolean;
    after?: boolean;
}

interface Options {
    before?: boolean;
    after?: boolean;
    overrides?: {
        colon?: WhitespaceRule;
        arrow?: WhitespaceRule;
    };
}

interface Issue {
    ruleFix: RuleFix;
    line: number;
    column: number;
    message: string;
    filePath: string;
}

export class TypeAnnotationSpacingCheck implements BaseChecker {
    public rule: Rule;
    private options: Options;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private defaultOptions: Options = {
        before: false,
        after: true,
        overrides: {
            colon: {
                before: false,
                after: true
            },
            arrow: {
                before: true,
                after: true
            }
        }
    };
    private issueMap: Map<string, IssueReport> = new Map();
    constructor() {
        this.options = this.defaultOptions;
    }
    registerMatchers(): MatcherCallback[] {
        const matchFileCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        }
        return [matchFileCb];
    }
    codeFix?(arkFile: ArkFile, fixKey: string): boolean {
        throw new Error("Method not implemented.");
    }

    public metaData: BaseMetaData = {
        severity: 1,
        ruleDocPath: 'docs/type-annotation-spacing-check.md',
        description: 'Require consistent spacing around type annotations'
    };

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE
    };

    public check = (target: ArkFile) => {
        this.issueMap.clear();
        if (this.rule && this.rule.option && this.rule.option.length > 0) {
            this.options = this.rule.option[0] as Options;
        }
        if (target instanceof ArkFile) {
            const code = target.getCode();
            this.checkTypeAnnotationSpacing(code, target).forEach((issue) => {
                issue.filePath = target.getFilePath();
                this.addIssueReport(issue);
            });
        }
    }

    private checkTypeAnnotationSpacing(code: string, arkFile: ArkFile): Issue[] {
        const sourceFile = AstTreeUtils.getASTNode(arkFile.getName(), code);
        const tempIssues: Issue[] = [];

        const checkNode = (node: ts.Node): void => {
            if (ts.isTypeNode(node) || ts.isPropertySignature(node) || ts.isPropertyDeclaration(node)) {
                const children = node.parent.getChildren(sourceFile);
                const questionColonToken = children.find(child => child.kind === ts.SyntaxKind.QuestionToken);
                if (questionColonToken) {
                    const quesTokenStart = questionColonToken.getStart(sourceFile);
                    const quesTokenEnd = questionColonToken.getEnd();
                    const tokenText = questionColonToken.getText(sourceFile);

                    let textBetweenTokens: string = '';
                    let cursor = quesTokenStart;
                    let loopTimes = 1;
                    for (; ;) {
                        textBetweenTokens = sourceFile.text.substring(quesTokenStart, cursor++);
                        if (textBetweenTokens.includes(':')) {
                            break
                        }
                        if (loopTimes > 100) {
                            break;
                        }
                        loopTimes++;
                    }

                    let quesBeforeSpace: boolean = false;
                    let quesAfterSpace: boolean = false;
                    const betweenSpace = textBetweenTokens.includes(' ');
                    if (betweenSpace) {
                        const quesTextBeforeToken = sourceFile.text.substring(quesTokenStart - 1, quesTokenStart);
                        quesBeforeSpace = quesTextBeforeToken === ' ';
                        const quesTextAfterToken = sourceFile.text.substring(quesTokenStart + loopTimes - 1, quesTokenStart + loopTimes);
                        quesAfterSpace = quesTextAfterToken === ' ';
                    } else {
                        const quesTextBeforeToken = sourceFile.text.substring(quesTokenStart - 1, quesTokenStart);
                        quesBeforeSpace = quesTextBeforeToken === ' ';
                        const quesTextAfterToken = sourceFile.text.substring(quesTokenEnd + 1, quesTokenEnd + 2);
                        quesAfterSpace = quesTextAfterToken === ' ';
                    }

                    let tempRule = this.options.overrides?.colon ?? {
                        before: this.options.before,
                        after: this.options.after
                    }
                    const lineAndChar = sourceFile.getLineAndCharacterOfPosition(quesTokenStart);
                    let pos = questionColonToken.pos;
                    let end = questionColonToken.end;
                    if (!betweenSpace && quesBeforeSpace !== tempRule.before) {
                        tempRule.before ? (end -= 1) : (end -= 1);
                        const fix: RuleFix = tempRule.before ? { range: [pos, end], text: ' ' } : { range: [pos, end], text: '' };
                        tempIssues.push({
                            ruleFix: fix,
                            line: lineAndChar.line + 1,
                            column: lineAndChar.character + 1,
                            message: tempRule.before
                                ? `Expected a space before the '${tokenText}'`
                                : `Unexpected space before the '${tokenText}'`,
                            filePath: arkFile.getFilePath() ?? ""
                        });
                    }
                    if (!betweenSpace && quesAfterSpace !== tempRule.after) {
                        tempRule.after ? (pos += 3, end += 1) : (end -= 1);
                        const fix: RuleFix = tempRule.after ? { range: [pos, end], text: ' ' } : { range: [pos, end], text: '' };
                        tempIssues.push({
                            ruleFix: fix,
                            line: lineAndChar.line + 1,
                            column: lineAndChar.character + 2,
                            message: tempRule.after
                                ? `Expected a space after the '${tokenText}'`
                                : `Unexpected space after the '${tokenText}'`,
                            filePath: arkFile.getFilePath() ?? ""
                        });
                    }
                    if (betweenSpace) {
                        if (quesBeforeSpace !== tempRule.before) {
                            tempRule.before ? (end -= 2) : (end -= 1);
                            const fix: RuleFix = tempRule.before ? { range: [pos, end], text: ' ' } : { range: [pos, end], text: '' };
                            tempIssues.push({
                                ruleFix: fix,
                                line: lineAndChar.line + 1,
                                column: lineAndChar.character + 1,
                                message: tempRule.before
                                    ? `Expected a space before the '${tokenText}'`
                                    : `Unexpected space before the '${tokenText}'`,
                                filePath: arkFile.getFilePath() ?? ""
                            });
                        }
                        const fix: RuleFix = { range: [quesTokenStart, quesTokenEnd], text: '?:' };
                        tempIssues.push({
                            ruleFix: fix,
                            line: lineAndChar.line + 1,
                            column: lineAndChar.character + 3,
                            message: `Unexpected space between '?' and ':'`,
                            filePath: arkFile.getFilePath() ?? ""
                        });
                        if (quesAfterSpace !== tempRule.after) {
                            tempRule.after ? (end -= 2) : (end -= 1);
                            const fix: RuleFix = tempRule.after ? { range: [pos, end], text: ' ' } : { range: [pos, end], text: '' };
                            tempIssues.push({
                                ruleFix: fix,
                                line: lineAndChar.line + 1,
                                column: lineAndChar.character + 1,
                                message: tempRule.after
                                    ? `Expected a space after the '${tokenText}'`
                                    : `Unexpected space after the '${tokenText}'`,
                                filePath: arkFile.getFilePath() ?? ""
                            });
                        }
                    }
                }
            }

            if (ts.isTypeNode(node)) {
                // 获取类型注解前的标记(冒号或箭头)
                const children = node.parent.getChildren(sourceFile);
                const questionColonToken = children.find(child => child.kind === ts.SyntaxKind.QuestionToken);
                if (questionColonToken) {
                    return;
                }

                const precedingToken = node.parent.getChildren(sourceFile).find(child =>
                    child.kind === ts.SyntaxKind.ColonToken ||
                    child.kind === ts.SyntaxKind.EqualsGreaterThanToken
                );

                if (precedingToken) {
                    const tokenStart = precedingToken.getStart(sourceFile);
                    const tokenEnd = precedingToken.getEnd();
                    const tokenText = precedingToken.getText(sourceFile);
                    const isArrow = precedingToken.kind === ts.SyntaxKind.EqualsGreaterThanToken;

                    const textBeforeToken = sourceFile.text.substring(tokenStart - 1, tokenStart);
                    const textAfterToken = sourceFile.text.substring(tokenEnd, tokenEnd + 1);
                    const beforeSpace = textBeforeToken === ' ';
                    const afterSpace = textAfterToken === ' ';
                    const rules = isArrow
                        ? (this.options.overrides?.arrow ?? {
                            before: this.options.before,
                            after: this.options.after
                        })
                        : (this.options.overrides?.colon ?? {
                            before: this.options.before,
                            after: this.options.after
                        });

                    if (rules) {
                        const lineAndChar = sourceFile.getLineAndCharacterOfPosition(tokenStart);
                        let pos = precedingToken.pos;
                        let end = precedingToken.end;
                        if (beforeSpace !== rules.before) {
                            if (isArrow) {
                                rules.before ? (end -= 2) : (end -= 2);
                            } else {
                                rules.before ? (end -= 1) : (end -= 1);
                            }
                            const fix: RuleFix = rules.before ? { range: [pos, end], text: ' ' } : { range: [pos, end], text: '' };
                            tempIssues.push({
                                ruleFix: fix,
                                line: lineAndChar.line + 1,
                                column: lineAndChar.character + 1,
                                message: rules.before
                                    ? `Expected a space before the '${tokenText}'`
                                    : `Unexpected space before the '${tokenText}'`,
                                filePath: arkFile.getFilePath() ?? ""
                            });
                        }

                        rules.after ? (pos += 2, end += 1) : (end -= 1);
                        if (afterSpace !== rules.after) {
                            if (isArrow) {
                                rules.after ? (end -= 1) : (pos += 3, end += 2);
                            } else {
                                rules.after ? (pos -= 1, end -= 1) : (pos += 1, end += 2);
                            }
                            const fix: RuleFix = rules.after ? { range: [pos, end], text: ' ' } : { range: [pos, end], text: '' };
                            tempIssues.push({
                                ruleFix: fix,
                                line: lineAndChar.line + 1,
                                column: lineAndChar.character + 1,
                                message: rules.after
                                    ? `Expected a space after the '${tokenText}'`
                                    : `Unexpected space after the '${tokenText}'`,
                                filePath: arkFile.getFilePath() ?? ""
                            });
                        }
                    }
                }
            }

            ts.forEachChild(node, checkNode);
        };

        checkNode(sourceFile);
        return tempIssues;
    }

    private addIssueReport(issue: Issue) {
        this.metaData.description = issue.message;
        const severity = this.rule.alert ?? this.metaData.severity;
        const fixKey = issue.line + '%' + issue.column + '%' + this.rule.ruleId;
        const mergeKey = issue.filePath + '%' + fixKey + '%' + this.metaData.description;
        const defect = new Defects(issue.line, issue.column, this.metaData.description, severity,
            this.rule.ruleId, mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey);
        RuleListUtil.push(defect);
        const fix: RuleFix = issue.ruleFix;
        let issueReport: IssueReport = { defect, fix };
        this.issues.push(issueReport);
    }
}